﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom.Compiler;
using System.IO;

namespace csis.ImmediatorClasses
{
	public class Compiler
	{
		public Compiler(CompilerOptions options)
		{
			if (options == null) throw new ArgumentNullException("options");
			this.options = options;
			this.CompileErrors = new List<CompileError>();
		}

		private readonly CompilerOptions options;

		public List<CompileError> CompileErrors { get; private set; }

		public Action<CompilerErrorCollection> CompileErrorHandler { get; set; }


		/// <summary>
		/// Contains the scripts that have been compiled, by their original input file name.
		/// </summary>
		public Dictionary<string, Script> CompiledScripts { get; private set; }

		/// <summary>
		/// Contains the scripts that have been compiled as a temporary file, due to
		/// preprocessing transformations applied to them, by their temporary file name.
		/// </summary>
		private Dictionary<string, Script> temporaryScripts;

		private CompilerPreparer compilerPreparer;

		public bool Compile()
		{
			this.compilerPreparer = CompilerPreparer.Run(this.options);

			if (this.compilerPreparer.InputScripts.Count == 0)
			{
				// no input files specified
				this.AddNoInputFilesError();
				return false;
			}

			this.CompiledScripts = new Dictionary<string, Script>();
			this.temporaryScripts = new Dictionary<string, Script>();

			foreach (var script in this.compilerPreparer.InputScripts)
			{
				this.CompiledScripts.Add(script.FilePath.ToLower(), script);
				if (script.TemporaryScript != null)
				{
					this.temporaryScripts.Add(script.TemporaryScript.FileName.ToLower(), script);
				}
			}

			// compilation
			var results = this.CompileWithCodeDom();

			var success = !this.TransformCompileErrors(results);

			return success;
		}

		private void AddNoInputFilesError()
		{
			this.CompileErrors.Add(new CompileError() { ErrorMessage = "No input files specified." });
		}

		private CompilerResults CompileWithCodeDom()
		{
			var provider = CodeDomProvider.CreateProvider("cs");

			var compilerParameters = new CompilerParameters();
			compilerParameters.OutputAssembly = this.options.OutputFile;
			compilerParameters.GenerateExecutable = true;
			compilerParameters.WarningLevel = this.options.CompilerWarningLevel;

			foreach (var assembly in this.compilerPreparer.ReferencedAssemblies)
			{
				compilerParameters.ReferencedAssemblies.Add(assembly);
			}

			var compiledFiles = this.compilerPreparer.InputScripts.Select(x => x.GetCompiledFilePath()).ToArray();

			var results = provider.CompileAssemblyFromFile(compilerParameters, compiledFiles);

			return results;
		}

		/// <summary>
		/// Un-applies code transformations like quickheader and automain.
		/// Returns whether the operation was an overall success (true; no errors, warnings are OK),
		/// or it has failed (false; at least one error).
		/// </summary>
		private bool TransformCompileErrors(CompilerResults compilerResults)
		{
			bool haveError = false;
			
			foreach (CompilerError error in compilerResults.Errors)
			{
				if (!error.IsWarning)
				{
					haveError = true;
				}
				string originalFileName = error.FileName;
				int originalLine = error.Line;
				Script temporaryScript;
				if (this.temporaryScripts.TryGetValue(error.FileName.ToLower(), out temporaryScript))
				{
					originalFileName = temporaryScript.FilePath;
					if (temporaryScript.AutoMain.HasValue && originalLine > temporaryScript.AutoMain.Value)
					{
						originalLine -= LineCount(Properties.Resources.AutoMainTemplateStart);
					}
					if (temporaryScript.UseQuickHeader)
					{
						originalLine -= LineCount(Properties.Resources.QuickHeader);
					}
				}

				var smooth = new CompileError();
				smooth.Column = error.Column;
				smooth.CompiledFile = error.FileName;
				smooth.CompiledLine = error.Line;
				smooth.ErrorCode = error.ErrorNumber;
				smooth.ErrorMessage = error.ErrorText;
				smooth.IsWarning = error.IsWarning;
				smooth.OriginalFile = originalFileName;
				smooth.OriginalLine = originalLine;
				this.CompileErrors.Add(smooth);
			}

			return haveError;
		}

		/// <summary>
		/// Counts the occurrences of \n in a string.
		/// </summary>
		public static int LineCount(string s)
		{
			int count = 0;
			int position = 0;
			while ((position = s.IndexOf('\n', position)) != -1)
			{
				count++;
				position++;
			}
			return count;
		}
	}
}
